home *** CD-ROM | disk | FTP | other *** search
- "use strict";
- /*
- * Copyright (C) 2012 Google Inc. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
- /**
- * @constructor
- * @param {!Element} element
- * @param {!Object} config
- */
- function SuggestionPicker(element, config) {
- Picker.call(this, element, config);
- this._isFocusByMouse = false;
- this._containerElement = null;
- this._setColors();
- this._layout();
- this._fixWindowSize();
- this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
- document.body.addEventListener("keydown", this._handleBodyKeyDownBound);
- this._element.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
- }
- SuggestionPicker.prototype = Object.create(Picker.prototype);
-
- SuggestionPicker.NumberOfVisibleEntries = 20;
-
- // An entry needs to be at least this many pixels visible for it to be a visible entry.
- SuggestionPicker.VisibleEntryThresholdHeight = 4;
-
- SuggestionPicker.ActionNames = {
- OpenCalendarPicker: "openCalendarPicker"
- };
-
- SuggestionPicker.ListEntryClass = "suggestion-list-entry";
-
- SuggestionPicker.validateConfig = function(config) {
- if (config.showOtherDateEntry && !config.otherDateLabel)
- return "No otherDateLabel.";
- if (config.suggestionHighlightColor && !config.suggestionHighlightColor)
- return "No suggestionHighlightColor.";
- if (config.suggestionHighlightTextColor && !config.suggestionHighlightTextColor)
- return "No suggestionHighlightTextColor.";
- if (config.suggestionValues.length !== config.localizedSuggestionValues.length)
- return "localizedSuggestionValues.length must equal suggestionValues.length.";
- if (config.suggestionValues.length !== config.suggestionLabels.length)
- return "suggestionLabels.length must equal suggestionValues.length.";
- if (typeof config.inputWidth === "undefined")
- return "No inputWidth.";
- return null;
- };
-
- SuggestionPicker.prototype._setColors = function() {
- var text = "." + SuggestionPicker.ListEntryClass + ":focus {\
- background-color: " + this._config.suggestionHighlightColor + ";\
- color: " + this._config.suggestionHighlightTextColor + "; }";
- text += "." + SuggestionPicker.ListEntryClass + ":focus .label { color: " + this._config.suggestionHighlightTextColor + "; }";
- document.head.appendChild(createElement("style", null, text));
- };
-
- SuggestionPicker.prototype.cleanup = function() {
- document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
- };
-
- /**
- * @param {!string} title
- * @param {!string} label
- * @param {!string} value
- * @return {!Element}
- */
- SuggestionPicker.prototype._createSuggestionEntryElement = function(title, label, value) {
- var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
- entryElement.tabIndex = 0;
- entryElement.dataset.value = value;
- var content = createElement("span", "content");
- entryElement.appendChild(content);
- var titleElement = createElement("span", "title", title);
- content.appendChild(titleElement);
- if (label) {
- var labelElement = createElement("span", "label", label);
- content.appendChild(labelElement);
- }
- entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
- return entryElement;
- };
-
- /**
- * @param {!string} title
- * @param {!string} actionName
- * @return {!Element}
- */
- SuggestionPicker.prototype._createActionEntryElement = function(title, actionName) {
- var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
- entryElement.tabIndex = 0;
- entryElement.dataset.action = actionName;
- var content = createElement("span", "content");
- entryElement.appendChild(content);
- var titleElement = createElement("span", "title", title);
- content.appendChild(titleElement);
- entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
- return entryElement;
- };
-
- /**
- * @return {!number}
- */
- SuggestionPicker.prototype._measureMaxContentWidth = function() {
- // To measure the required width, we first set the class to "measuring-width" which
- // left aligns all the content including label.
- this._containerElement.classList.add("measuring-width");
- var maxContentWidth = 0;
- var contentElements = this._containerElement.getElementsByClassName("content");
- for (var i=0; i < contentElements.length; ++i) {
- maxContentWidth = Math.max(maxContentWidth, contentElements[i].offsetWidth);
- }
- this._containerElement.classList.remove("measuring-width");
- return maxContentWidth;
- };
-
- SuggestionPicker.prototype._fixWindowSize = function() {
- var ListBorder = 2;
- var desiredWindowWidth = this._measureMaxContentWidth() + ListBorder;
- if (typeof this._config.inputWidth === "number")
- desiredWindowWidth = Math.max(this._config.inputWidth, desiredWindowWidth);
- var totalHeight = ListBorder;
- var maxHeight = 0;
- var entryCount = 0;
- for (var i = 0; i < this._containerElement.childNodes.length; ++i) {
- var node = this._containerElement.childNodes[i];
- if (node.classList.contains(SuggestionPicker.ListEntryClass))
- entryCount++;
- totalHeight += node.offsetHeight;
- if (maxHeight === 0 && entryCount == SuggestionPicker.NumberOfVisibleEntries)
- maxHeight = totalHeight;
- }
- var desiredWindowHeight = totalHeight;
- if (maxHeight !== 0 && totalHeight > maxHeight) {
- this._containerElement.style.maxHeight = (maxHeight - ListBorder) + "px";
- desiredWindowWidth += getScrollbarWidth();
- desiredWindowHeight = maxHeight;
- this._containerElement.style.overflowY = "scroll";
- }
-
- var windowRect = adjustWindowRect(desiredWindowWidth, desiredWindowHeight, desiredWindowWidth, 0);
- this._containerElement.style.height = (windowRect.height - ListBorder) + "px";
- setWindowRect(windowRect);
- };
-
- SuggestionPicker.prototype._layout = function() {
- if (this._config.isRTL)
- this._element.classList.add("rtl");
- if (this._config.isLocaleRTL)
- this._element.classList.add("locale-rtl");
- this._containerElement = createElement("ul", "suggestion-list");
- this._containerElement.addEventListener("click", this._handleEntryClick.bind(this), false);
- for (var i = 0; i < this._config.suggestionValues.length; ++i) {
- this._containerElement.appendChild(this._createSuggestionEntryElement(this._config.localizedSuggestionValues[i], this._config.suggestionLabels[i], this._config.suggestionValues[i]));
- }
- if (this._config.showOtherDateEntry) {
- // Add separator
- var separator = createElement("div", "separator");
- this._containerElement.appendChild(separator);
-
- // Add "Other..." entry
- var otherEntry = this._createActionEntryElement(this._config.otherDateLabel, SuggestionPicker.ActionNames.OpenCalendarPicker);
- this._containerElement.appendChild(otherEntry);
- }
- this._element.appendChild(this._containerElement);
- };
-
- /**
- * @param {!Element} entry
- */
- SuggestionPicker.prototype.selectEntry = function(entry) {
- if (typeof entry.dataset.value !== "undefined") {
- this.submitValue(entry.dataset.value);
- } else if (entry.dataset.action === SuggestionPicker.ActionNames.OpenCalendarPicker) {
- window.addEventListener("didHide", SuggestionPicker._handleWindowDidHide, false);
- hideWindow();
- }
- };
-
- SuggestionPicker._handleWindowDidHide = function() {
- openCalendarPicker();
- window.removeEventListener("didHide", SuggestionPicker._handleWindowDidHide);
- };
-
- /**
- * @param {!Event} event
- */
- SuggestionPicker.prototype._handleEntryClick = function(event) {
- var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
- if (!entry)
- return;
- this.selectEntry(entry);
- event.preventDefault();
- };
-
- /**
- * @return {?Element}
- */
- SuggestionPicker.prototype._findFirstVisibleEntry = function() {
- var scrollTop = this._containerElement.scrollTop;
- var childNodes = this._containerElement.childNodes;
- for (var i = 0; i < childNodes.length; ++i) {
- var node = childNodes[i];
- if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
- continue;
- if (node.offsetTop + node.offsetHeight - scrollTop > SuggestionPicker.VisibleEntryThresholdHeight)
- return node;
- }
- return null;
- };
-
- /**
- * @return {?Element}
- */
- SuggestionPicker.prototype._findLastVisibleEntry = function() {
- var scrollBottom = this._containerElement.scrollTop + this._containerElement.offsetHeight;
- var childNodes = this._containerElement.childNodes;
- for (var i = childNodes.length - 1; i >= 0; --i){
- var node = childNodes[i];
- if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
- continue;
- if (scrollBottom - node.offsetTop > SuggestionPicker.VisibleEntryThresholdHeight)
- return node;
- }
- return null;
- };
-
- /**
- * @param {!Event} event
- */
- SuggestionPicker.prototype._handleBodyKeyDown = function(event) {
- var eventHandled = false;
- var key = event.keyIdentifier;
- if (key === "U+001B") { // ESC
- this.handleCancel();
- eventHandled = true;
- } else if (key == "Up") {
- if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
- for (var node = document.activeElement.previousElementSibling; node; node = node.previousElementSibling) {
- if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
- this._isFocusByMouse = false;
- node.focus();
- break;
- }
- }
- } else {
- this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":last-child").focus();
- }
- eventHandled = true;
- } else if (key == "Down") {
- if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
- for (var node = document.activeElement.nextElementSibling; node; node = node.nextElementSibling) {
- if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
- this._isFocusByMouse = false;
- node.focus();
- break;
- }
- }
- } else {
- this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":first-child").focus();
- }
- eventHandled = true;
- } else if (key === "Enter") {
- this.selectEntry(document.activeElement);
- eventHandled = true;
- } else if (key === "PageUp") {
- this._containerElement.scrollTop -= this._containerElement.clientHeight;
- // Scrolling causes mouseover event to be called and that tries to move the focus too.
- // To prevent flickering we won't focus if the current focus was caused by the mouse.
- if (!this._isFocusByMouse)
- this._findFirstVisibleEntry().focus();
- eventHandled = true;
- } else if (key === "PageDown") {
- this._containerElement.scrollTop += this._containerElement.clientHeight;
- if (!this._isFocusByMouse)
- this._findLastVisibleEntry().focus();
- eventHandled = true;
- }
- if (eventHandled)
- event.preventDefault();
- };
-
- /**
- * @param {!Event} event
- */
- SuggestionPicker.prototype._handleEntryMouseOver = function(event) {
- var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
- if (!entry)
- return;
- this._isFocusByMouse = true;
- entry.focus();
- event.preventDefault();
- };
-
- /**
- * @param {!Event} event
- */
- SuggestionPicker.prototype._handleMouseOut = function(event) {
- if (!document.activeElement.classList.contains(SuggestionPicker.ListEntryClass))
- return;
- this._isFocusByMouse = false;
- document.activeElement.blur();
- event.preventDefault();
- };
-